/*
 * Copyright (c) 2025 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import { ts, ArkFile, AstTreeUtils, Local, UnclearReferenceType, FunctionType, ArkAliasTypeDefineStmt, AliasType, UnionType, ArkClass, ArkAssignStmt, Type, VoidType, Value, ClassType, ArkNewExpr, ClassSignature, NeverType, ArrayType, GenericType, ArkInvokeStmt, IntersectionType, UnknownType } from "arkanalyzer/lib";
import Logger, { LOG_MODULE_TYPE } from 'arkanalyzer/lib/utils/logger';
import { BaseChecker, BaseMetaData } from "../BaseChecker";
import { Defects, IssueReport } from "../../model/Defects";
import { FileMatcher, MatcherCallback, MatcherTypes } from "../../matcher/Matchers";
import { Rule } from "../../model/Rule";
import { RuleListUtil } from "../../utils/common/DefectsList";
import { KeyofTypeExpr } from "arkanalyzer/lib/core/base/TypeExpr";

const logger = Logger.getLogger(LOG_MODULE_TYPE.HOMECHECK, 'NoInvalidVoidTypeCheck');
const gMetaData: BaseMetaData = {
    severity: 2,
    ruleDocPath: "docs/no-invalid-void-type-check.md",
    description: 'Disallow `void` type outside of generic or return types',
    messages: {
        invalidVoidForGeneric:
            '{{ generic }} may not have void as a type argument.',
        invalidVoidNotReturn: 'void is only valid as a return type.',
        invalidVoidNotReturnOrGeneric:
            'void is only valid as a return type or generic type argument.',
        invalidVoidNotReturnOrThisParam:
            'void is only valid as return type or type of `this` parameter.',
        invalidVoidNotReturnOrThisParamOrGeneric:
            'void is only valid as a return type or generic type argument or the type of a `this` parameter.',
        invalidVoidUnionConstituent:
            'void is not valid as a constituent in a union type',
    },
    defaultOptions: { allowInGenericTypeArguments: true }
};

enum CheckType {
    ReturnType,
    GenericTypeArgument,
    ThisParameter,
    leftOp,
    rightOp,
    DefaultCase,
    NewExpr,
    Constraint, UnionType
}

type Options = {
    allowAsThisParameter?: boolean;
    allowInGenericTypeArguments?: [string, ...string[]] | boolean;
}

export class NoInvalidVoidTypeCheck implements BaseChecker {
    readonly metaData: BaseMetaData = gMetaData;
    public rule: Rule;
    public defects: Defects[] = [];
    public issues: IssueReport[] = [];
    private fileMatcher: FileMatcher = {
        matcherType: MatcherTypes.FILE,
    };

    public registerMatchers(): MatcherCallback[] {
        const fileCb: MatcherCallback = {
            matcher: this.fileMatcher,
            callback: this.check
        }
        return [fileCb];
    }

    private getOption(): Options {
        let option = this.metaData.defaultOptions;
        if (this.rule && this.rule.option[0]) {
            option = this.rule.option[0] as Options;
        }
        return option;
    }

    private getMessage(arkfile: ArkFile, option: Options): { line: number, colum: number, message: string, sourceCode: string, generic?: string }[] {
        let errorPositions: { line: number, colum: number, message: string, sourceCode: string, generic?: string }[] = [];
        const allowInGenericTypeArguments = option.allowInGenericTypeArguments;
        const allowAsThisParameter = option.allowAsThisParameter;
        let invalidType = [ClassType, AliasType, UnclearReferenceType, ArrayType];

        arkfile.getClasses().forEach(cls => {
            cls.getFields().forEach(field => {
                checkVoidType(field.getSignature().getType(), field.getOriginPosition().getLineNo(), field.getCode(), CheckType.ThisParameter);
            })
            let useMethods: string[] = [];
            cls.getMethods().forEach(method => {
                //这些方法在其他表达式中已使用
                if (useMethods.includes(method.getName())) {
                    return;
                }
                method.getBody()?.getCfg().getStmts()?.forEach(stmt => {
                    let originalText = stmt.getOriginalText() ?? '';
                    let line = stmt.getOriginPositionInfo().getLineNo();
                    if (stmt instanceof ArkAliasTypeDefineStmt) {
                        checkVoidType(stmt.getAliasType().getOriginalType(), line, originalText, CheckType.GenericTypeArgument);
                    }
                    if (stmt instanceof ArkAssignStmt) {
                        let rightType = stmt.getRightOp().getType();
                        checkVoidType(stmt.getLeftOp().getType(), line, originalText, CheckType.leftOp);
                        if (rightType instanceof FunctionType) {
                            useMethods.push(rightType.getMethodSignature().getMethodSubSignature().getMethodName());
                        }
                        if (stmt.getRightOp() instanceof ArkNewExpr) {
                            checkVoidType(rightType, line, originalText, CheckType.NewExpr);
                        } else if (!(stmt.getRightOp() instanceof Local)) {
                            checkVoidType(rightType, line, originalText, CheckType.rightOp);
                        }
                    }
                    if (stmt instanceof ArkInvokeStmt) {
                        stmt.getInvokeExpr().getRealGenericTypes()?.forEach(genericType => {
                            checkVoidType(genericType, line, originalText, CheckType.GenericTypeArgument);
                        })
                    }
                })
                let methodLine = method.getLine() ?? method.getDeclareLines()?.at(0);
                let methodCode = method.getCode();
                if (!methodLine || !methodCode) {
                    return;
                }
                for (const parameter of method.getParameters()) {
                    if (allowAsThisParameter && parameter.getType().getTypeString() == 'void' && parameter.getName() == 'this') {
                        return;
                    }
                    checkVoidType(parameter.getType(), methodLine, methodCode, CheckType.ThisParameter);
                }
                method.getGenericTypes()?.forEach(genericType => {
                    checkVoidType(genericType, methodLine, methodCode, CheckType.GenericTypeArgument);
                });
                if (method.getReturnType().getTypeString().includes('void')) {
                    if (method.getReturnType() instanceof VoidType) {
                        return;
                    }
                    checkVoidType(method.getReturnType(), methodLine, methodCode, CheckType.ReturnType);
                }
            });
        });

        function getASTNodePosition(code: string, type: CheckType): { line: number, colum: number } {
            let position: { line: number, colum: number } = { line: 0, colum: 0 };
            let astTree = AstTreeUtils.getASTNode('temp', code);

            function typeCheck(node: ts.TypeNode) {
                if (node.kind == ts.SyntaxKind.VoidKeyword) {
                    let { line, character } = astTree.getLineAndCharacterOfPosition(node.getStart())
                    position = { line: line + 1, colum: character + 1 };
                }
                if (ts.isUnionTypeNode(node)) {
                    node.types.forEach(ty => {
                        typeCheck(ty);
                    })
                }
                if (ts.isArrayTypeNode(node)) {
                    typeCheck(node.elementType);
                }
                if (ts.isConditionalTypeNode(node)) {
                    typeCheck(node.checkType);
                    typeCheck(node.extendsType);
                    typeCheck(node.trueType);
                    typeCheck(node.falseType);
                }

                if (ts.isTypeOperatorNode(node)) {
                    typeCheck(node.type);
                }
                if (ts.isTypeReferenceNode(node) && node.typeArguments) {
                    node.typeArguments.forEach(argument => {
                        typeCheck(argument);
                    })
                }
                if ((ts.isPropertyDeclaration(node) || ts.isPropertySignature(node)) && node.type) {
                    typeCheck(node.type);
                }
            }
            let shouldStop = false;
            function getPosition(node: ts.Node) {
                if (shouldStop) return;
                // 添加一个标志变量
                if ((ts.isFunctionDeclaration(node) || ts.isArrowFunction(node) || ts.isMethodDeclaration(node) || ts.isFunctionTypeNode(node) || ts.isFunctionLike(node))) {
                    if (type == CheckType.ReturnType && node.type) {
                        typeCheck(node.type);
                        shouldStop = true;
                        return;
                    }
                    if (type == CheckType.ThisParameter && node.parameters.length > 0) {
                        node.parameters.forEach(param => {
                            if (param.type) typeCheck(param.type);
                        })
                        shouldStop = true;
                        return;
                    }
                }

                if (ts.isTypeParameterDeclaration(node)) {
                    if (type == CheckType.Constraint && node.constraint) {
                        typeCheck(node.constraint);
                        shouldStop = true;
                        return;
                    }
                    if (type == CheckType.DefaultCase && node.default) {
                        typeCheck(node.default);
                        shouldStop = true;
                        return;
                    }

                }
                if (ts.isMappedTypeNode(node) && node.type) {
                    typeCheck(node.type);
                    shouldStop = true;
                    return;
                }

                if (type == CheckType.ThisParameter && node.kind == ts.SyntaxKind.VoidExpression) {
                    let { line, character } = astTree.getLineAndCharacterOfPosition(node.getStart())
                    position = { line: line + 1, colum: character + 1 };
                    shouldStop = true;
                    return;
                }
                if (ts.isCallExpression(node)) {
                    for (const typeArgument of node.typeArguments ?? []) {
                        typeCheck(typeArgument);
                        shouldStop = true;
                        return;
                    }
                }

                if (ts.isVariableDeclaration(node)) {
                    if (node.type && type == CheckType.leftOp) {
                        typeCheck(node.type);
                        shouldStop = true;
                        return;
                    }
                    if (node.initializer && ts.isNewExpression(node.initializer) && node.initializer.typeArguments && type == CheckType.rightOp) {
                        node.initializer.typeArguments.forEach(argument => {
                            typeCheck(argument);
                        });
                        shouldStop = true;
                        return;
                    }
                }
                if (ts.isTypeNode(node)) {
                    typeCheck(node);
                }
                if (!shouldStop) {
                    node.forEachChild(getPosition);
                }
            }
            getPosition(astTree);
            return position;
        }
        function report(line: number, colum: number, messageId: string, generic?: string) {
            errorPositions.push({ line: line, colum: colum, message: messageId, sourceCode: 'void', generic });
        }
        function checkGenericTypeArgument(type: Type, line: number, originalText: string, checkType: CheckType, genericName?: string) {
            if (invalidType.some(inType => type instanceof inType) || type instanceof GenericType) {
                if (Array.isArray(allowInGenericTypeArguments)) {
                    if (!genericName) {
                        return;
                    }
                    genericName = genericName?.replace(/ /gu, '');
                    if (!allowInGenericTypeArguments.map(s => s.replace(/ /gu, '')).includes(genericName)) {
                        let position = getASTNodePosition(originalText, checkType);
                        if (position.line != 0) {
                            report(position.line + line - 1, position.colum, 'invalidVoidForGeneric', genericName);
                        }
                    }
                    return;
                }
                if (!allowInGenericTypeArguments) {
                    let position = getASTNodePosition(originalText, checkType);
                    if (position.line != 0) {
                        report(position.line + line - 1, position.colum, allowAsThisParameter ? 'invalidVoidNotReturnOrThisParam' : 'invalidVoidNotReturn');
                    }
                }
            }
        }

        function isValidUnionType(type: UnionType): boolean {
            return type.getTypes().every(member => {
                let types;
                if (member instanceof ClassType || member instanceof AliasType) {
                    types = member.getRealGenericTypes();
                }
                if (member instanceof UnclearReferenceType) {
                    types = member.getGenericTypes();
                }
                return ['void', 'never'].includes(member.getTypeString()) ||
                    (types && types.some(type => type.getTypeString() == 'void'));
            });
        }
        function getNotReturnOrGenericMessageId(type: Type): string {
            return type instanceof UnionType ?
                'invalidVoidUnionConstituent' : 'invalidVoidNotReturnOrGeneric';
        }
        function checkVoidType(type: Type, line: number, originalText: string, checkType: CheckType) {
            let position;
            if (type instanceof ClassType || type instanceof AliasType || type instanceof UnclearReferenceType) {
                let genericName = type instanceof ClassType ? type.getClassSignature().getClassName().toString() : type.getName();
                let types = type instanceof UnclearReferenceType ? type.getGenericTypes() : type.getRealGenericTypes();
                if (checkType == CheckType.NewExpr) {
                    if (allowInGenericTypeArguments == true) {
                        return;
                    }
                    types?.forEach(type => {
                        checkVoidType(type, line, originalText, checkType);
                    })
                    return;
                }

                if (types?.some(ty => ty instanceof VoidType)) {
                    checkGenericTypeArgument(type, line, originalText, checkType, genericName);
                    return;
                }
            }
            if (type instanceof IntersectionType) {
                type.getTypes().forEach(type => {
                    checkVoidType(type, line, originalText, checkType);
                })
            }

            if (type instanceof KeyofTypeExpr) {
                checkVoidType(type.getOpType(), line, originalText, checkType);
            }
            if (type instanceof FunctionType) {
                type.getMethodSignature().getMethodSubSignature().getParameters().forEach(parameter => {
                    checkVoidType(parameter.getType(), line, originalText, checkType);
                });
            }
            if (type instanceof GenericType) {
                const defaultType = type.getDefaultType();
                const constraint = type.getConstraint();
                if (defaultType) {
                    checkGenericTypeArgument(type, line, originalText, CheckType.DefaultCase);
                }
                if (constraint) {
                    checkVoidType(constraint, line, originalText, CheckType.Constraint);
                }
            }

            if (type instanceof ArrayType) {
                if (type.getBaseType() instanceof VoidType) {
                    if (/Array<.*?>/.test(originalText)) {
                        checkGenericTypeArgument(type, line, originalText, checkType, 'Array');
                    } else {
                        checkVoidType(type.getBaseType(), line, originalText, checkType);
                    }
                }
            }

            if (type instanceof UnionType && isValidUnionType(type)) {
                for (const ty of type.getTypes()) {
                    if (!(ty instanceof VoidType)) {
                        checkVoidType(ty, line, originalText, checkType);
                    }
                }
                return;
            }

            if (type instanceof UnknownType) {
                position = getASTNodePosition(originalText, checkType);
                if (position.colum == 0) return;
            }
            if (type instanceof VoidType || type instanceof UnionType || position) {
                if (!position) {
                    position = getASTNodePosition(originalText, checkType);
                }
                let messageId =
                    allowInGenericTypeArguments && allowAsThisParameter ?
                        'invalidVoidNotReturnOrThisParamOrGeneric' : allowInGenericTypeArguments ?
                            getNotReturnOrGenericMessageId(type) : allowAsThisParameter ?
                                'invalidVoidNotReturnOrThisParam' : 'invalidVoidNotReturn';
                if (position.line != 0) {
                    report(position.line + line - 1, position.colum, messageId);
                }
            }
        }
        return errorPositions;
    }

    public check = (target: ArkFile) => {
        let options = this.getOption();
        let errorPositions: { line: number, colum: number, message: string, sourceCode: string, generic?: string }[] = this.getMessage(target, options);
        // 对 errorPositions 进行排序
        errorPositions.sort((a, b) => {
            if (a.line !== b.line) {
                return a.line - b.line; // 先按行号排序
            }
            return a.colum - b.colum; // 行号相同时按列号排序
        });
        errorPositions.forEach(position => {
            this.addIssueReport(target, position.line, position.colum, position.sourceCode, position.message, position.generic);
        });
    }

    private addIssueReport(arkFile: ArkFile, lineNum: number, startColum: number, code: string, messageId: string, generic: string = '') {
        const severity = this.rule.alert ?? this.metaData.severity;
        let message = messageId !== 'invalidVoidForGeneric' ? this.metaData.messages[messageId] : `${generic} may not have void as a type argument.`;
        let filePath = arkFile.getFilePath();
        let endColum = startColum + code.length - 1;
        let defect = new Defects(lineNum, startColum, endColum, message, severity, this.rule.ruleId, filePath, this.metaData.ruleDocPath, true, false, false);
        this.issues.push(new IssueReport(defect, undefined));
        RuleListUtil.push(defect);
    }
}


